package com.cyj.minio.util;

import cn.hutool.core.date.DateUtil;
import cn.hutool.core.lang.UUID;
import cn.hutool.core.util.RandomUtil;
import cn.hutool.core.util.StrUtil;
import com.cyj.minio.config.MinIoProperties;
import com.google.common.io.ByteStreams;
import io.minio.*;
import io.minio.errors.MinioException;
import io.minio.http.Method;
import io.minio.messages.Item;
import lombok.extern.slf4j.Slf4j;
import org.springframework.stereotype.Component;

import javax.annotation.PostConstruct;
import javax.annotation.Resource;
import java.io.*;
import java.util.*;
import java.util.stream.Collectors;

/**
 * @Description: MinIo工具类
 * @BelongsProject: SpringBoot-MinIO
 * @BelongsPackage: com.cyj.minio.util
 * @Author: ChenYongJia
 * @CreateTime: 2021-09-13 13:59
 * @Email: chen87647213@163.com
 * @Version: 1.0
 */
@Slf4j
@Component
public class MinIoUtils {
    @Resource
    private MinIoProperties minIoProperties;

    private static String url;

    private static String accessKey;

    private static String secretKey;

    public static String chunkBucKet;

    public static String originFileBucKet;

    public static String allowOriginFileBucKet;

    private static MinioClient minioClient;

    /**
     * 排序
     */
    public final static boolean SORT = true;

    /**
     * 不排序
     */
    public final static boolean NOT_SORT = false;

    /**
     * 默认过期时间(分钟)
     */
    private final static Integer DEFAULT_EXPIRY = 60;

    public static void afterPropertiesSet() throws Exception {
        minioClient = MinioClient.builder().endpoint(url).credentials(accessKey, secretKey).build();
        //方便管理分片文件，则单独创建一个分片文件的存储桶
        if (!StrUtil.isEmpty(chunkBucKet) && !isBucketExist(chunkBucKet)) {
            createBucket(chunkBucKet);
        }
        if (!StrUtil.isEmpty(originFileBucKet) && !isBucketExist(originFileBucKet)) {
            createBucket(originFileBucKet);
        }
        if (!StrUtil.isEmpty(allowOriginFileBucKet) && !isBucketExist(allowOriginFileBucKet)) {
            createBucket(allowOriginFileBucKet);
        }
    }


    public static MinioClient getInstance() throws Exception {

        if (minioClient == null) {
            synchronized (MinIoUtils.class) {
                if (minioClient == null) {
                    afterPropertiesSet();
                }
            }
        }
        return minioClient;
    }

    /**
     * 初始化MinIo对象
     */
    @PostConstruct
    public void init() {
        url = minIoProperties.getUrl();
        accessKey = minIoProperties.getAccessKey();
        secretKey = minIoProperties.getSecretKey();
        chunkBucKet = minIoProperties.getChunkBucKetValue();
        originFileBucKet = minIoProperties.getOriginFileBucKetValue();
        allowOriginFileBucKet = minIoProperties.getAllowOriginFileBucKetValue();
    }

    /**
     * 存储桶是否存在
     *
     * @param bucketName 存储桶名称
     * @return true/false
     */
    public static boolean isBucketExist(String bucketName) throws Exception {
        return getInstance().bucketExists(BucketExistsArgs.builder().bucket(bucketName).build());
    }

    /**
     * 上传对象到minio，目标位置和存储名称存在重复时会覆盖
     *
     * @param bucketName 目标桶
     * @param filePath   目标位置和存储名称，如 tempdir/123.txt
     * @param file
     */
    public static void putObject(String bucketName, String filePath, File file) throws Exception {
        getInstance().putObject(
                PutObjectArgs.builder()
                        .bucket(bucketName)
                        .object(removeSlash(filePath))
                        .stream(new FileInputStream(file), file.length(), -1)
                        .build()
        );
    }

    /**
     * 桶内文件复制
     *
     * @param bucketName     目标桶
     * @param sourceFilePath 目标位置
     * @param targetFilePath 复制到
     */
    public static boolean copyObject(String bucketName, String sourceFilePath, String targetFilePath) throws Exception {

        try {
            getInstance().copyObject(CopyObjectArgs.builder()
                    .bucket(bucketName)
                    .object(targetFilePath)
                    .source(CopySource
                            .builder()
                            .bucket(bucketName)
                            .object(sourceFilePath)
                            .build()
                    )
                    .build());
            return true;
        } catch (MinioException e) {
            System.out.println("Error occurred: " + e);
            return false;
        }

    }

    /**
     * 上传对象到minio，目标位置和存储名称存在重复时会覆盖
     *
     * @param bucketName 目标桶
     * @param filePath   目标位置和存储名称，如 tempdir/123.txt
     * @param file
     */
    public static void putObject(String bucketName, String filePath, InputStream file) throws Exception {
        getInstance().putObject(
                PutObjectArgs.builder()
                        .bucket(bucketName)
                        .object(removeSlash(filePath))
                        .stream(file, file.available(), -1)
                        .build()
        );
    }

    private static String removeSlash(String str) {
        if (str.substring(0, 1).equals("/")) {
            return str.substring(1);
        }
        return str;
    }

    /**
     * 从minio下载指定路径对象，目标位置和存储名称存在重复时会覆盖
     *
     * @param bucketName 目标桶
     * @param filePath   目标位置和存储名称，如 tempdir/123.txt
     */
    public static byte[] getObject(String bucketName, String filePath) throws Exception {

        InputStream inputStream = null;
        try {
            inputStream = getInstance().getObject(
                    GetObjectArgs.builder()
                            .bucket(bucketName)
                            .object(removeSlash(filePath))
                            .build()
            );
        } catch (MinioException e) {
            System.out.println("Error occurred: " + e);
            return null;
        }
        ByteArrayOutputStream outputStream = new ByteArrayOutputStream();
        {
            ByteStreams.copy(inputStream, outputStream);
            byte[] buffer = outputStream.toByteArray();
            return buffer;
        }
    }

    /**
     * 获取文件状态信息
     *
     * @param bucketName 目标桶
     * @param filePath   目标位置和存储名称，如 tempdir/123.txt
     */
    public static StatObjectResponse statObject(String bucketName, String filePath) throws Exception {

        try {
            return getInstance().statObject(
                    StatObjectArgs.builder()
                            .bucket(bucketName)
                            .object(removeSlash(filePath))
                            .build()
            );
        } catch (MinioException e) {
            System.out.println("Error occurred: " + e);
            return null;
        }
    }

    /**
     * 移除文件
     *
     * @param bucketName 目标桶
     * @param filePath   目标位置和存储名称，如 tempdir/123.txt
     */
    public static void removeObject(String bucketName, String filePath) throws Exception {
        getInstance().removeObject(
                RemoveObjectArgs.builder()
                        .bucket(bucketName)
                        .object(removeSlash(filePath))
                        .build()
        );
    }

    /**
     * 创建存储桶
     *
     * @param bucketName 存储桶名称
     * @return true/false
     */
    public static boolean createBucket(String bucketName) throws Exception {
        getInstance().makeBucket(MakeBucketArgs.builder().bucket(bucketName).build());
        return true;
    }

    /**
     * 获取访问对象的外链地址
     *
     * @param objectName 对象名称
     * @param expiry     过期时间(分钟) 最大为7天 超过7天则默认最大值
     * @return viewUrl
     */
    public static String getOriginalObjectUrl(String objectName, Integer expiry) throws Exception {
        return getObjectUrl(originFileBucKet, objectName, expiry);
    }

    /**
     * 获取访问对象的外链地址
     *
     * @param bucketName 存储桶名称
     * @param objectName 对象名称
     * @param expiry     过期时间(分钟) 最大为7天 超过7天则默认最大值
     * @return viewUrl
     */
    public static String getObjectUrl(String bucketName, String objectName, Integer expiry) throws Exception {
        expiry = expiryHandle(expiry);
        return getInstance().getPresignedObjectUrl(
                GetPresignedObjectUrlArgs.builder()
                        .method(Method.GET)
                        .bucket(bucketName)
                        .object(removeSlash(objectName))
                        .expiry(expiry)
                        .build()
        );
    }

    /**
     * 创建上传文件对象的外链
     *
     * @param bucketName 存储桶名称
     * @param objectName 欲上传文件对象的名称
     * @param expiry     过期时间(分钟) 最大为7天 超过7天则默认最大值
     * @return uploadUrl
     */
    public static String createUploadUrl(String bucketName, String objectName, Integer expiry) throws Exception {
        expiry = expiryHandle(expiry);
        return getInstance().getPresignedObjectUrl(
                GetPresignedObjectUrlArgs.builder()
                        .method(Method.PUT)
                        .bucket(bucketName)
                        .object(removeSlash(objectName))
                        .expiry(expiry)
                        .build()
        );
    }

    /**
     * 创建上传文件对象的外链
     *
     * @param bucketName 存储桶名称
     * @param objectName 欲上传文件对象的名称
     * @return uploadUrl
     */
    public static String createUploadUrl(String bucketName, String objectName) throws Exception {
        return createUploadUrl(bucketName, objectName, DEFAULT_EXPIRY);
    }

    /**
     * 批量创建分片上传外链
     *
     * @param bucketName 存储桶名称
     * @param objectMD5  欲上传分片文件主文件的MD5
     * @param chunkCount 分片数量
     * @return uploadChunkUrls
     */
    public static List<String> createUploadChunkUrlList(String bucketName, String objectMD5, Integer chunkCount) throws Exception {
        if (null == bucketName) {
            bucketName = chunkBucKet;
        }
        if (null == objectMD5) {
            return null;
        }
        objectMD5 += "/";
        if (null == chunkCount || 0 == chunkCount) {
            return null;
        }
        List<String> urlList = new ArrayList<>(chunkCount);
        for (int i = 1; i <= chunkCount; i++) {
            String objectName = objectMD5 + i + ".chunk";
            urlList.add(createUploadUrl(bucketName, objectName, DEFAULT_EXPIRY));
        }
        return urlList;
    }

    /**
     * 创建指定序号的分片文件上传外链
     *
     * @param bucketName 存储桶名称
     * @param objectMD5  欲上传分片文件主文件的MD5
     * @param partNumber 分片序号
     * @return uploadChunkUrl
     */
    public static String createUploadChunkUrl(String bucketName, String objectMD5, Integer partNumber) throws Exception {
        if (null == bucketName) {
            bucketName = chunkBucKet;
        }
        if (null == objectMD5) {
            return null;
        }
        objectMD5 += "/" + partNumber + ".chunk";
        return createUploadUrl(bucketName, objectMD5, DEFAULT_EXPIRY);
    }

    /**
     * 获取对象文件名称列表
     *
     * @param bucketName 存储桶名称
     * @param prefix     对象名称前缀
     * @param sort       是否排序(升序)
     * @return objectNames
     */
    public static List<String> listObjectNames(String bucketName, String prefix, Boolean sort) throws Exception {
        ListObjectsArgs listObjectsArgs;
        if (null == prefix) {
            listObjectsArgs = ListObjectsArgs.builder()
                    .bucket(bucketName)
                    .recursive(true)
                    .build();
        } else {
            listObjectsArgs = ListObjectsArgs.builder()
                    .bucket(bucketName)
                    .prefix(prefix)
                    .recursive(true)
                    .build();
        }
        Iterable<Result<Item>> chunks = getInstance().listObjects(listObjectsArgs);
        List<String> chunkPaths = new ArrayList<>();
        for (Result<Item> item : chunks) {
            chunkPaths.add(item.get().objectName());
        }
        if (sort) {
            return chunkPaths.stream().distinct().collect(Collectors.toList());
        }
        return chunkPaths;
    }

    /**
     * 获取对象文件名称列表
     *
     * @param bucketName 存储桶名称
     * @param prefix     对象名称前缀
     * @return objectNames
     */
    public static List<String> listObjectNames(String bucketName, String prefix) throws Exception {
        return listObjectNames(bucketName, prefix, NOT_SORT);
    }

    /**
     * 获取分片文件名称列表
     *
     * @param bucketName 存储桶名称
     * @param ObjectMd5  对象Md5
     * @return objectChunkNames
     */
    public static List<String> listChunkObjectNames(String bucketName, String ObjectMd5) throws Exception {
        if (null == bucketName) {
            bucketName = chunkBucKet;
        }
        if (null == ObjectMd5) {
            return null;
        }
        return listObjectNames(bucketName, ObjectMd5, SORT);
    }

    /**
     * 获取分片名称地址HashMap key=分片序号 value=分片文件地址
     *
     * @param bucketName 存储桶名称
     * @param ObjectMd5  对象Md5
     * @return objectChunkNameMap
     */
    public static Map<Integer, String> mapChunkObjectNames(String bucketName, String ObjectMd5) throws Exception {
        if (null == bucketName) {
            bucketName = chunkBucKet;
        }
        if (null == ObjectMd5) {
            return null;
        }
        List<String> chunkPaths = listObjectNames(bucketName, ObjectMd5);
        if (null == chunkPaths || chunkPaths.size() == 0) {
            return null;
        }
        Map<Integer, String> chunkMap = new HashMap<>(chunkPaths.size());
        for (String chunkName : chunkPaths) {
            Integer partNumber = Integer.parseInt(chunkName.substring(chunkName.indexOf("/") + 1, chunkName.lastIndexOf(".")));
            chunkMap.put(partNumber, chunkName);
        }
        return chunkMap;
    }

    /**
     * 合并分片文件成对象文件
     *
     * @param chunkBucKetName   分片文件所在存储桶名称
     * @param composeBucketName 合并后的对象文件存储的存储桶名称
     * @param chunkNames        分片文件名称集合
     * @param objectName        合并后的对象文件名称
     * @return true/false
     */
    public static boolean composeObject(String chunkBucKetName, String composeBucketName, List<String> chunkNames, String objectName) throws Exception {
        if (null == chunkBucKetName) {
            chunkBucKetName = chunkBucKet;
        }
        List<ComposeSource> sourceObjectList = new ArrayList<>(chunkNames.size());
        for (String chunk : chunkNames) {
            sourceObjectList.add(
                    ComposeSource.builder()
                            .bucket(chunkBucKetName)
                            .object(chunk)
                            .build()
            );
        }
        getInstance().composeObject(
                ComposeObjectArgs.builder()
                        .bucket(composeBucketName)
                        .object(removeSlash(objectName))
                        .sources(sourceObjectList)
                        .build()
        );
        return true;
    }

    /**
     * 合并分片文件成对象文件
     *
     * @param chunkNames 分片文件名称集合
     * @param objectName 合并后的对象文件名称
     * @return true/false
     */
    public static boolean composeObject(List<String> chunkNames, String objectName) throws Exception {
        return composeObject(chunkBucKet, originFileBucKet, chunkNames, objectName);
    }

    /**
     * 直接上传
     *
     * @param fileName    文件名
     * @param inputStream 文件流
     * @return
     * @throws Exception
     */
    public static String upload(String fileName, InputStream inputStream, String fileBucketName) throws Exception {
        String suffix = fileName.substring(fileName.lastIndexOf(".") + 1);
        return upload(inputStream, suffix, fileBucketName);
    }

    /**
     * 直接上传
     *
     * @param inputStream 文件流
     * @param suffix      文件后缀
     * @return
     * @throws Exception
     */
    public static String upload(InputStream inputStream, String suffix, String fileBucketName) throws Exception {
        String uuid = UUID.randomUUID().toString();
        String savePath = getSavePath(uuid + "." + suffix);
        putObject(fileBucketName, savePath, inputStream);
        return savePath;
    }

    private static String getSavePath(String fileName) {

        String dayStr = DateUtil.now();
        String days = dayStr.substring(0, dayStr.lastIndexOf(" "));
        String[] dayArr = days.split("-");

        String path = dayArr[0] + "/" + dayArr[1] + "/" + dayArr[2] + "/" + fileName;

        return path;
    }

    /**
     * 将分钟数转换为秒数
     *
     * @param expiry 过期时间(分钟数)
     * @return expiry
     */
    private static int expiryHandle(Integer expiry) {
        expiry = expiry * 60;
        if (expiry > 604800) {
            return 604800;
        }
        return expiry;
    }

}
